package BNS.plumbing;

import java.lang.Thread;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.awt.BorderLayout;
import java.io.Serializable;
import javax.swing.JApplet;

/** This class is the base of the applet that operates in the player machine.
	It contacts the BNS server, polls it for changes in BNSPersona, installs
	them when they come, and otherwise keeps the current Persona updated with
	fresh state data from the game running on the server.
	*/
public class Client extends JApplet implements Runnable {
	/**
   * 
   */
  private static final long serialVersionUID = 1L;
  BNSPersona persona;
	ClientLiaisonInterface clientLiaison;
	private int clientID;
	//private boolean flipPanel;
	private int terminateReason;
	private SessionHostInterface host;
	private Thread clientThread;
	private String playerName;
	
	/** Initializes the applet and installs a TextPersona.
		This method is required by the Applet interface. */
	public void init() {
		playerName= getParameter("playername");
		getContentPane().setLayout(new BorderLayout());
		persona= new TextPersona("BNSClient");
		getContentPane().add("Center",persona.startPanel());
		validate();
		clientID= -1;
		if (host!=null) System.err.println("HEY! this is a re-init()! ??");
		host= null;
		}
	
	/** Attempts to resign from the game.
		This method is part of the Applet interface. */
	public void destroy() { 
		System.err.println("Client destroy()ed!");
		if (host!=null) {
			try { host.resign(clientLiaison,"applet destroyed"); showStatus("resigned"); }
			catch(java.rmi.RemoteException e) { 
				System.err.println("couldn't resign! "+e); showStatus("RemoteException"); }
			catch(Exception e) { 
				System.err.println("couldn't resign! "+e); showStatus("Exception"); }
			host=null;
			}
		//if (persona!=null) remove(persona.killPanel());
		}
	
	/* creates and starts a Thread to operate the persona. Part of the Applet interface. */
	public void start() {
		System.err.println("Client start()ed");
		clientThread= new Thread(this);
		clientThread.start();
		}
	
	/* stops the Thread that operates the persona. Part of the Applet interface. */
	public void stop() {
		System.err.println("Client stop()ed!");
		terminateReason= 3;
		/* TODO: interrupt client to avoid time spent in TimeAdaptor.delay(),
		which increases likelihood that it won't resign in time. */
		//clientThread.interrupt();  
		/* The line above causes the exception on following line:
	java.security.AccessControlException: access denied (java.lang.RuntimePermission modifyThread)
		I don't understand it. */
		}
	
	/** Part of the Applet interface; returns an informative string. */
	public String getAppletInfo() {
		return "Title: BNS Client \nAuthor: J S Judd";
		}
	
	/** Used by the browser to describe stuff to the user. */
	public String[][] getParameterInfo() {
		return new String[][]{{"name","type","desc"}};
		}
	
	/** Contains the main loop operating the Client GUI.
		Initially attempts to contact server looking for the BNS service.
		If that succeeds, a client number is obtained and a contact message displayed to the player.
		Then a loop is entered that constantly polls the ClientLiaison object in the server
		by calling its passUpdate() method.
		Normal causes for this loop to exit are a null Envelope returned from 
		the call to clientLiaison.passUpdate(),  and a stop() call from the browser.
		*/
	public void run() {
		terminateReason= 0;
		String url= "//"+ getCodeBase().getHost() + "/"+ Constants.serviceName;
		try {
			showStatus("connecting");
			boolean connected= false;
			while(!connected) {
				// TODO: put a server poke test in here
				System.err.println("looking up "+ url);
				persona.gameUpdate((Serializable)("looking up "+url+"\n"));
				try { host= (SessionHostInterface)Naming.lookup(url); 
					persona.gameUpdate((Serializable)("Naming.lookup got host\n  "+host+"\n"));
					persona.gameUpdate((Serializable)("gonna register\n"));
					clientLiaison= (ClientLiaisonInterface)host.register(playerName);
//					host.register(playerName);
					connected=true; 
					}
				catch(NotBoundException e) {
					persona.gameUpdate((Serializable)(e+"\n"+e.getCause()+"\n")); 
					Thread.sleep(10000); }
				catch(java.rmi.ConnectException ce) {
					persona.gameUpdate((Serializable)(ce+"\n")); 
					Thread.sleep(2000); }
				catch(Exception e) {
					persona.gameUpdate((Serializable)(e+"\n"+e.getCause()+"\n")); 
					Thread.sleep(20000); }
				persona.gameUpdate((Serializable)("----------------------------------------\n"));
				}
			System.err.println(" got Liaison "+ clientLiaison);
			persona.gameUpdate((Serializable)("now registered as client #"+ clientID +"\n"));
			persona.gameUpdate((Serializable)(" clientLiaison "+ clientLiaison));
			persona.gameUpdate((Serializable)(" seeking Persona for game\n"));
			showStatus("starting");
			TimeAdaptor timer= new TimeAdaptor();
			int cycleTime= 1000;
			while (terminateReason==0) {
				Serializable envelope= null;
//				System.err.println(" getting envelope ");
				try { envelope= clientLiaison.passUpdate(); }
				catch(java.security.AccessControlException e) {
					System.err.println("message: "+e.getMessage());
					System.err.println("cause: "+e.getCause());
					System.err.println("Permission.name: "+e.getPermission().getName());
					System.err.println("Permission.actions: "+e.getPermission().getActions());
					terminateReason=1;
					break;
					}
//				System.err.println(" envelope= "+envelope);
				if (envelope==null) { terminateReason=2; break; }	// games over; resign
				if (envelope instanceof BNSPersona) {
//					System.err.println(" envelope is instanceof");
					persona.stop();
					remove(persona.killPanel());
					persona= (BNSPersona)envelope;
					System.err.println("got BNSPersona "+ persona.getTitle());				
					getContentPane().add(persona.startPanel());
					validate();		// *parent* of persona must lay out all components
					persona.start();
					cycleTime= persona.getCycleTime();
					}
				else 
					persona.gameUpdate(envelope);
//				if (envelope.status!=null) persona.setMessage(envelope.status);
				timer.delay(cycleTime);	// let server do other work
				}
			try { host.resign(clientLiaison,"no more games"); }
			catch(Exception e) { 
				System.err.println("Ignoring exception during resignation. "+e.getMessage()); }
			host=null;
			try { showStatus("resigned"); }
			catch(NullPointerException e) {}	// I don't know how this sometimes happens.
			persona.stop();
			remove(persona.killPanel());
			persona= new TextPersona("Thank you for your participation");
			getContentPane().add("Center", persona.startPanel());
			validate();
			switch(terminateReason) {	// reveals how loop was terminated
			case 1: System.err.println("Client terminated due to Exception"); break;
			case 2: System.err.println("Client terminated on request from BNSHost"); break;
			case 3: System.err.println("Client terminated on request from browser"); break;
			default: System.err.println("Client terminated for no reason!"); break;
				}
			}
		catch (InterruptedException ex) {
			System.err.println("something interrupted the client "+ ex); }
//		catch (MalformedURLException ex) {
//			System.err.println(url + " is not a valid RMI URL"); }
		catch (RemoteException ex) {
			System.err.println("Remote object threw exception " + ex);
			ex.printStackTrace(); }
		} // EOMethod run()
	
	
	class TimeAdaptor {
		long lastTime= System.currentTimeMillis(); int delay=100;
		double work1=10, work2=10;

		public void delay(int desired) {
			delay= (int)(desired-work2);
			//System.err.println("delay ="+desired+"-"+work2+" = "+delay);
			if (delay>0) 
				try { Thread.sleep(delay); }
				catch (InterruptedException ex) {
					System.err.println("something interrupted the client "+ ex); }
			else { delay= 0; Thread.yield(); }
			long nextTime= System.currentTimeMillis();
			long thisTime= nextTime-lastTime;
			
			long thisWork= thisTime-delay;
			if (thisWork>work1+10) thisWork= (long)(work1+10);
			work1= work1*.9 + thisWork*.1;
			work2= work2*.99 + thisWork*.01;
			//if (work2<3) System.err.println("Yikes! work2 time is "+work2+"!");
			String msg= "cycle time "+ pad(thisTime)+
					   "; work time "+pad(thisWork)+", "+pad(work1)+", "+pad(work2);
			try { showStatus(msg); }
			catch(NullPointerException e) {}	// browser might clear status bar pointer
			lastTime= nextTime;
			}
		
		private String pad(double val) { return pad((long)val); }
		private String pad(long val) { String s= "     "+val; return s.substring(s.length()-5); }
			
		} // EOC TimeAdaptor
	} // EOC Client
